<!-- VirtualScroller.vue -->
<template>
  <div
    ref="containerRef"
    class="virtual-list-container"
    :style="{ height: containerHeight }"
    @scroll="handleScroll"
  >
    <div class="phantom" :style="{ height: phantomHeight + 'px' }"></div>
    <div
      class="content"
      :style="{ transform: `translate3d(0, ${startOffset}px, 0)` }"
    >
      <div
        v-for="item in visibleData"
        :key="item.id"
        :data-index="item.index"
        ref="itemsRef"
      >
        <slot :item="item.data"></slot>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUpdated, type Ref } from 'vue'

interface Position {
  index: number
  height: number
  top: number
  bottom: number
}

const props = withDefaults(
  defineProps<{
    listData: any[]
    estimatedItemSize: number
    containerHeight?: string
    bufferScale?: number
  }>(),
  {
    containerHeight: '100%',
    bufferScale: 1
  }
)

const emit = defineEmits(['scroll'])

// 响应式引用
const containerRef = ref<HTMLElement | null>(null)
const itemsRef = ref<HTMLElement[]>([])
const positions = ref<Position[]>([])
const startIndex = ref(0)
const endIndex = ref(0)
const startOffset = ref(0)

// 初始化位置缓存
const initPositions = () => {
  positions.value = props.listData.map((_, index) => ({
    index,
    height: props.estimatedItemSize,
    top: index * props.estimatedItemSize,
    bottom: (index + 1) * props.estimatedItemSize
  }))
}

// 计算属性
const phantomHeight = computed(() => positions.value[positions.value.length - 1]?.bottom || 0)
const visibleCount = computed(() => {
  const container = containerRef.value
  return container ? Math.ceil(container.clientHeight / props.estimatedItemSize) : 0
})
const aboveCount = computed(() => Math.min(startIndex.value, props.bufferScale * visibleCount.value))
const belowCount = computed(() =>
  Math.min(props.listData.length - endIndex.value, props.bufferScale * visibleCount.value)
)
const visibleData = computed(() => {
  const start = Math.max(0, startIndex.value - aboveCount.value)
  const end = Math.min(endIndex.value + belowCount.value, props.listData.length)
  return props.listData.slice(start, end).map((data, index) => ({
    id: `${start + index}-${data.id || index}`,
    index: start + index,
    data
  }))
})

// 二分查找起始索引
const binarySearch = (scrollTop: number): number => {
  let start = 0
  let end = positions.value.length - 1
  let tempIndex = -1

  while (start <= end) {
    const mid = Math.floor((start + end) / 2)
    const midBottom = positions.value[mid].bottom

    if (midBottom === scrollTop) {
      return mid + 1
    } else if (midBottom < scrollTop) {
      start = mid + 1
    } else {
      if (tempIndex === -1 || tempIndex > mid) {
        tempIndex = mid
      }
      end = mid - 1
    }
  }
  return tempIndex
}

// 更新尺寸
const updateItemsSize = () => {
  itemsRef.value.forEach(node => {
    const index = Number(node.dataset.index)
    const height = node.getBoundingClientRect().height
    if (positions.value[index] && height !== positions.value[index].height) {
      const diff = height - positions.value[index].height
      positions.value[index].height = height
      positions.value[index].bottom += diff

      for (let i = index + 1; i < positions.value.length; i++) {
        positions.value[i].top = positions.value[i - 1].bottom
        positions.value[i].bottom += diff
      }
    }
  })
}

// 滚动处理
const handleScroll = () => {
  if (!containerRef.value) return

  const scrollTop = containerRef.value.scrollTop
  startIndex.value = binarySearch(scrollTop)
  endIndex.value = startIndex.value + visibleCount.value

  // 计算偏移量
  if (startIndex.value >= 1) {
    const lastIndex = startIndex.value - 1
    startOffset.value = positions.value[lastIndex].bottom
  } else {
    startOffset.value = 0
  }

  emit('scroll', { scrollTop })
}

// 生命周期
onMounted(() => {
  initPositions()
  if (containerRef.value) {
    endIndex.value = startIndex.value + visibleCount.value
  }
})

onUpdated(() => {
  requestAnimationFrame(() => {
    updateItemsSize()
  })
})
</script>

<style scoped>
.virtual-list-container {
  position: relative;
  overflow-y: auto;
}

.phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.content {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
}
</style>